﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Text.RegularExpressions;

namespace HtmlExtractor
{
    /////////////////////////////////////////////////////////////////////////////
    // Copyright (c) 2001, Sells Brothers, Inc.
    // All rights reserved. No warranties extended. Use at your own risk.
    // You may not redistribute this code in any form.
    // The official home of this utility is http://www.sellsbrothers.com
    /////////////////////////////////////////////////////////////////////////////
    public enum Operation
    {
        None,
        Match,
        Replace,
        Split
    }

    #region SegmentMatch

    /// <summary>
    /// SegmentMatch
    /// </summary>
    public class SegmentMatch
    {
        public SegmentMatch(string originalValue, int number, bool isVisible, bool hasZeroLengthPrefix, SegmentMatchGroupCollection matchGroupCollection)
        {
            // Fill property
            _originalValue = originalValue;
            _isVisible = isVisible;
            _hasZeroLengthPrefix = hasZeroLengthPrefix;
            _number = number;
            _matchGroupCollection = matchGroupCollection;
        }
        /// <summary>
        /// 整个匹配值，等于匹配时的Segment.value
        /// </summary>
        public string OriginalValue
        {
            get { return _originalValue; }
        }

        public bool IsVisible
        {
            get { return _isVisible; }
        }

        public bool HasZeroLengthPrefix
        {
            get { return _hasZeroLengthPrefix; }
        }

        public int Number
        {
            get { return _number; }
        }

        public SegmentMatchGroupCollection MatchGroups
        {
            get { return _matchGroupCollection; }
        }

        string _originalValue;
        bool _isVisible;
        bool _hasZeroLengthPrefix;
        int _number;
        SegmentMatchGroupCollection _matchGroupCollection;
    }
    #endregion

    #region SegmentMatchGroup

    /// <summary>
    /// SegmentMatch
    /// </summary>
    public class SegmentMatchGroup
    {
        public SegmentMatchGroup(int position, string name, string value, bool named)
        {
            // Fill property
            _position = position;
            _name = name;
            _value = value;
            _named = named;
        }

        public int Position
        {
            get { return _position; }
        }

        public string Name
        {
            get { return _name; }
        }

        public string Value
        {
            get { return _value; }
        }

        public bool Named
        {
            get { return _named; }
        }

        int _position;
        string _name;
        string _value;
        bool _named;
    }
    #endregion

    #region SegmentMatchGroupCollection

    /// <summary>
    ///一次匹配中捕获组的集合
    /// </summary>
    public class SegmentMatchGroupCollection : ICollection
    {
        public SegmentMatchGroupCollection(SegmentMatchGroup[] segmentMatchGroupCollection)
        {
            _segmentMatchGroupCollection = segmentMatchGroupCollection;
        }

        #region ICollection

        public int Count
        {
            get { return _segmentMatchGroupCollection.Length; }
        }

        public void CopyTo(Array array, int index)
        {
            _segmentMatchGroupCollection.CopyTo(array, index);
        }

        public bool IsSynchronized
        {
            get { return false; }
        }

        public object SyncRoot
        {
            get { return null; }
        }

        #endregion

        #region IEnumerable

        public IEnumerator GetEnumerator()
        {
            return new SegmentMatchGroupEnumerator(_segmentMatchGroupCollection);
        }

        #endregion

        #region IEnumerator

        public class SegmentMatchGroupEnumerator : IEnumerator
        {
            public SegmentMatchGroupEnumerator(SegmentMatchGroup[] segmentMatchGroupCollection)
            {
                _segmentMatchGroupCollection = segmentMatchGroupCollection;
                Reset();
            }

            public object Current
            {
                get
                {
                    // If the enumerator points to an element, return it
                    if ((_segmentMatchGroupIndex < 0) || (_segmentMatchGroupIndex > _segmentMatchGroupCollection.Length - 1)) throw new InvalidOperationException();
                    return (SegmentMatchGroup)_segmentMatchGroupCollection[_segmentMatchGroupIndex];
                }
            }

            public bool MoveNext()
            {
                // Move enumerator to next element
                return (++_segmentMatchGroupIndex < _segmentMatchGroupCollection.Length);
            }

            public void Reset()
            {
                // Move enumerator to beginning of the collection, before the first element
                _segmentMatchGroupIndex = -1;
            }

            int _segmentMatchGroupIndex;
            SegmentMatchGroup[] _segmentMatchGroupCollection;
        }

        #endregion

        SegmentMatchGroup[] _segmentMatchGroupCollection;
    }

    #endregion

    #region Segment
    /// <summary>
    /// 所有结果，包含匹配，非匹配结果。如果是匹配的结果Match非null
    /// </summary>
    public class Segment
    {
        public Segment(int index, int length, string value)
        {
            // Fill properties
            _index = index;
            _length = length;
            _value = value;
        }

        public Segment(int index, int length, string value, SegmentMatch match)
        {
            // Fill properties
            _index = index;
            _length = length;
            _value = value;
            _match = match;
        }

        public int Index
        {
            get { return _index; }
        }

        public int Length
        {
            get { return _length; }
        }
        /// <summary>
        /// 值，如果是匹配的Segment，则是匹配值
        /// </summary>
        public string Value
        {
            get { return _value; }
        }
        public SegmentMatch Match
        {
            get { return _match; }
        }

        int _index;
        int _length;
        string _value;
        SegmentMatch _match;
    }
    #endregion


    public class OperationName
    {
        public string Singular = "No Operation";
        public string Plural = "No Operation";
    }

    #region SegmentCollection

    /// <summary>
    /// Segment Collection
    /// </summary>
    public class SegmentCollection : ICollection
    {
        #region Constructors

        public SegmentCollection(Operation operation, string input, string pattern, string replace, RegexOptions options, bool processMultiple)
        {
            // If not a Match, make sure Replace string isn't null
            if ((operation != Operation.Match) && (replace == null)) throw new ArgumentNullException("If a Replace or Split, Replace cannot be null.");

            // Build SegmentCollection according to SegmentMatch
            switch (operation)
            {
                case Operation.Match:
                    BuildMatchSegmentCollection(input, pattern, options, processMultiple);
                    break;

                default:
                    BuildMatchAndReplaceSegmentCollection(operation, input, pattern, replace, options, processMultiple);
                    break;
            }

            // Get operation details
            _operation = operation;
            _operationName = new OperationName();
            switch (_operation)
            {
                case Operation.Match:
                    _operationName.Singular = "Match";
                    _operationName.Plural = "Matches";
                    break;

                case Operation.Replace:
                    _operationName.Singular = "Replace";
                    _operationName.Plural = "Replaces";
                    break;

                case Operation.Split:
                    _operationName.Singular = "Split";
                    _operationName.Plural = "Splits";
                    break;
            }
        }

        private void BuildMatchSegmentCollection(string input, string pattern, RegexOptions options, bool processMultiple)
        {
            // Match
            Regex re = new Regex(pattern, options);
            MatchCollection matches = re.Matches(input);

            // Get number of matches
            if (!processMultiple && matches.Count > 0) _matchCount = 1;
            else _matchCount = matches.Count;

            // Build SegmentCollection
            bool reverse = processMultiple && ((options & RegexOptions.RightToLeft) != 0);
            int matchNumber = (reverse ? _matchCount + 1 : 0);
            int matchIncrement = (reverse ? -1 : 1);
            int offset = 0;
            List<Segment> segmentCollection = new List<Segment>();

            _zeroLengthMatchCount = 0;
            foreach (Match match in (reverse ? (IEnumerable)new ReverseCollection(matches) : matches))
            {
                // Get match item
                matchNumber = matchNumber + matchIncrement;

                // Add new segment (if any unmatched text prior to this match)
                int preTextLength = match.Index - offset;
                if (preTextLength > 0) segmentCollection.Add(new Segment(offset, preTextLength, input.Substring(offset, preTextLength)));

                // Add new Segment (with "Match" SegmentMatch)
                // NOTE: this catches the situation where a match is made
                //       that includes a zero-length group at the start of the match.
                //       In this situation, the match includes the zero-length match
                //       at the start of the match.  Unfortunately, we need to display
                //       this character and need some way to do it.
                if (match.Value == "") _zeroLengthMatchCount++;
                SegmentMatch segmentMatch = new SegmentMatch(match.Value, matchNumber, IsVisible(match.Value), HasZeroLengthPrefix(match), GetGroupNames(re, match));
                segmentCollection.Add(new Segment(match.Index, match.Length, match.Value, segmentMatch));

                // Calculate offset
                offset = match.Index + match.Length;

                // Bail if SingleMatch
                if (!processMultiple) break;
            }

            // Add new segment (if any unmatched text after the last match)
            int postTextLength = input.Length - offset;
            if (postTextLength > 0) segmentCollection.Add(new Segment(offset, postTextLength, input.Substring(offset)));

            // Copy to type-specific Segment array ie avoid late-binding
            _segmentCollection = new Segment[segmentCollection.Count];
            segmentCollection.CopyTo(_segmentCollection);
        }

        private void BuildMatchAndReplaceSegmentCollection(Operation operation, string input, string pattern, string replace, RegexOptions options, bool processMultiple)
        {
            // Match
            Regex re = new Regex(pattern, options);
            string replaced = re.Replace(input, replace);
            MatchCollection matches = re.Matches(input);

            // Get number of matches
            if (!processMultiple && matches.Count > 0) _matchCount = 1;
            else _matchCount = matches.Count;

            // Build SegmentCollection
            bool reverse = processMultiple && ((options & RegexOptions.RightToLeft) != 0);
            int matchNumber = (reverse ? matches.Count + 1 : 0);
            int matchIncrement = (reverse ? -1 : 1);
            int replacedOffset = 0;
            int inputOffset = 0;
            List<Segment> segmentCollection = new List<Segment>();

            _zeroLengthMatchCount = 0;
            foreach (Match match in (reverse ? (IEnumerable)new ReverseCollection(matches) : matches))
            {
                // Get match item
                matchNumber = matchNumber + matchIncrement;

                // Add new segment (if any unmatched text prior to this match)
                int preTextLength = match.Index - inputOffset;
                if (preTextLength > 0) segmentCollection.Add(new Segment(replacedOffset, preTextLength, input.Substring(inputOffset, preTextLength)));

                // Add new Segment (with "Replace" or "Split" SegmentMatch)
                // NOTE: HasZeroLengthPrefix() check is only relevant
                //       for Match, since Replace and Split do their thing 
                //       including the whole match, including this char
                if (match.Value == "") _zeroLengthMatchCount++;
                SegmentMatch segmentMatch = new SegmentMatch(match.Value, matchNumber, IsVisible(match.Value), false, GetGroupNames(re, match));
                segmentCollection.Add(new Segment(match.Index, replace.Length, replace, segmentMatch));

                // Calculate offset
                replacedOffset += replace.Length;
                inputOffset = match.Index + match.Length;

                // Bail if SingleMatch
                if (!processMultiple) break;
            }

            // Add new segment (if any unmatched text after the last match)
            int postTextLength = input.Length - inputOffset;
            if (postTextLength > 0) segmentCollection.Add(new Segment(replacedOffset, postTextLength, input.Substring(inputOffset)));

            // Copy to type-specific Segment array ie avoid late-binding
            _segmentCollection = new Segment[segmentCollection.Count];
            segmentCollection.CopyTo(_segmentCollection);
        }

        #endregion

        public int MatchCount
        {
            get { return _matchCount; }
        }

        public int ZeroLengthMatchCount
        {
            get { return _zeroLengthMatchCount; }
        }

        public Operation Operation
        {
            get { return _operation; }
        }

        public OperationName OperationName
        {
            get { return _operationName; }
        }

        #region ICollection

        public int Count
        {
            get { return _segmentCollection.Length; }
        }

        public void CopyTo(Array array, int index)
        {
            _segmentCollection.CopyTo(array, index);
        }

        public bool IsSynchronized
        {
            get { return false; }
        }

        public object SyncRoot
        {
            get { return null; }
        }

        #endregion

        #region IEnumerable

        public IEnumerator GetEnumerator()
        {
            return new SegmentEnumerator(_segmentCollection);
        }

        #endregion

        #region IEnumerator

        public class SegmentEnumerator : IEnumerator
        {
            public SegmentEnumerator(Segment[] segmentCollection)
            {
                _segmentCollection = segmentCollection;
                Reset();
            }

            public object Current
            {
                get
                {
                    // If the enumerator points to an element, return it
                    if ((_segmentIndex < 0) || (_segmentIndex > _segmentCollection.Length - 1)) throw new InvalidOperationException();
                    return (Segment)_segmentCollection[_segmentIndex];
                }
            }

            public bool MoveNext()
            {
                // Move enumerator to next element
                return (++_segmentIndex < _segmentCollection.Length);
            }

            public void Reset()
            {
                // Move enumerator to beginning of the collection, before the first element
                _segmentIndex = -1;
            }

            int _segmentIndex;
            Segment[] _segmentCollection;
        }

        #endregion

        private bool HasZeroLengthPrefix(Match match)
        {
            // Return true if a match
            // * has 1 or more characters
            // * has a zero-length group at (0, 0) in the match
            // * and the group was successfully matched
            string matchValue = match.Value;
            foreach (Group group in match.Groups)
            {
                if ((group.Length == 0) &&
                    (group.Index == 0) &&
                    (group.Success == true) &&
                    (matchValue != "")) return true;
            }
            return false;
        }

        private SegmentMatchGroupCollection GetGroupNames(Regex re, Match match)
        {
            // Get named groups
            List<object> matchGroups = new List<object>();
            for (int groupIndex = 1; groupIndex < match.Groups.Count; groupIndex++)
            {
                if (match.Groups[groupIndex].Success == true)
                {
                    string groupName = re.GroupNameFromNumber(groupIndex);
                    matchGroups.Add(new SegmentMatchGroup(groupIndex, groupName, match.Groups[groupIndex].Value, (groupIndex.ToString() != groupName)));
                }
            }
            SegmentMatchGroup[] segmentMatchGroupList = new SegmentMatchGroup[matchGroups.Count];
            matchGroups.CopyTo(segmentMatchGroupList, 0);

            return new SegmentMatchGroupCollection(segmentMatchGroupList);
        }

        private bool IsVisible(string text)
        {
            if (text.Length == 0) return false;

            // If white-space character, can't really be visible
            Regex re = new Regex(@"[^\s]");
            if (!re.IsMatch(text)) return false;
            else return true;
        }

        Segment[] _segmentCollection;
        int _matchCount;
        int _zeroLengthMatchCount;
        Operation _operation;
        OperationName _operationName;
    }

    #endregion
}
